package csc.transformation;

import csc.main.Colors;
import csc.triangulation.Vector3d;

/**
 * 
 * @author Stanislau Dubrouski
 */
public class Interpolator {

    /**
     * Calculates color intensity for given coordinates, using specified interpolation method and color channel
     * @param image original image
     * @param coordinates coordinates of point to get color for
     * @param color color channel
     * @param method interpolation method to be used
     * @return color intensity for specified color channel and coordinates, calculated with specified interpolation method.
     */
    static public int interpolate(int[][][] image, Vector3d coordinates, Colors color, InterpolationMethod method) {

        switch (method) {
            case NEAREST_NEIGHBOR:
                return computeNearestNeighbor(image, coordinates, color);
            case TRILINEAR:
                return computeTriLinear(image, coordinates, color);
            case TRICUBIC:
                return computeTriCubic(image, coordinates, color);
            default:
                return computeNearestNeighbor(image, coordinates, color);
        }
    }

    private static int computeNearestNeighbor(int[][][] image, Vector3d coordinates, Colors color) {
        //if coordinates are overlapping image size, make corrections
        coordinates = correctCoordinates(coordinates, image);

        //round coordinates to nearest int value
        Vector3d rc = new Vector3d();
        rc.setX(Math.round(coordinates.getX()));
        rc.setY(Math.round(coordinates.getY()));
        rc.setZ(Math.round(coordinates.getZ()));

        //get requested color and return
        switch (color) {
            case RED:
                int red = (int) (image[(int) rc.getX()][(int) rc.getY()][(int) rc.getZ()] & 0xff0000) >> 16;
                return red;
            case GREEN:
                int green = (int) (image[(int) rc.getX()][(int) rc.getY()][(int) rc.getZ()] & 0x00ff00) >> 8;
                return green;
            case BLUE:
                int blue = (int) (image[(int) rc.getX()][(int) rc.getY()][(int) rc.getZ()] & 0x0000ff);
                return blue;
        }
        return 0;
    }

    private static int computeTriLinear(int[][][] image, Vector3d coordinates, Colors color) {
        //if coordinates are overlapping image size, make corrections
        coordinates = correctCoordinates(coordinates, image);
        float x = coordinates.getX() % 1;
        float y = coordinates.getY() % 1;
        float z = coordinates.getZ() % 1;

        int lowerX = (int) (coordinates.getX() - (coordinates.getX() % 1));
        int lowerY = (int) (coordinates.getY() - (coordinates.getY() % 1));
        int lowerZ = (int) (coordinates.getZ() - (coordinates.getZ() % 1));

        int upperX = (int) Math.ceil(coordinates.getX());
        int upperY = (int) Math.ceil(coordinates.getY());
        int upperZ = (int) Math.ceil(coordinates.getZ());

        int lXlYlZ;
        int lXlYuZ;
        int lXuYlZ;
        int lXuYuZ;
        int uXlYlZ;
        int uXlYuZ;
        int uXuYlZ;
        int uXuYuZ;

        //get requested color and return
        switch (color) {
            case RED:
                lXlYlZ = (int) (image[lowerX][lowerY][lowerZ] & 0xff0000) >> 16;
                lXlYuZ = (int) (image[lowerX][lowerY][upperZ] & 0xff0000) >> 16;
                lXuYlZ = (int) (image[lowerX][upperY][lowerZ] & 0xff0000) >> 16;
                lXuYuZ = (int) (image[lowerX][upperY][upperZ] & 0xff0000) >> 16;
                uXlYlZ = (int) (image[upperX][lowerY][lowerZ] & 0xff0000) >> 16;
                uXlYuZ = (int) (image[upperX][lowerY][upperZ] & 0xff0000) >> 16;
                uXuYlZ = (int) (image[upperX][upperY][lowerZ] & 0xff0000) >> 16;
                uXuYuZ = (int) (image[upperX][upperY][upperZ] & 0xff0000) >> 16;
                break;
            case GREEN:
                lXlYlZ = (int) (image[lowerX][lowerY][lowerZ] & 0x00ff00) >> 8;
                uXlYlZ = (int) (image[upperX][lowerY][lowerZ] & 0x00ff00) >> 8;
                lXlYuZ = (int) (image[lowerX][lowerY][upperZ] & 0x00ff00) >> 8;
                uXlYuZ = (int) (image[upperX][lowerY][upperZ] & 0x00ff00) >> 8;
                lXuYlZ = (int) (image[lowerX][upperY][lowerZ] & 0x00ff00) >> 8;
                uXuYlZ = (int) (image[upperX][upperY][lowerZ] & 0x00ff00) >> 8;
                lXuYuZ = (int) (image[lowerX][upperY][upperZ] & 0x00ff00) >> 8;
                uXuYuZ = (int) (image[upperX][upperY][upperZ] & 0x00ff00) >> 8;
                break;
            case BLUE:
                lXlYlZ = (int) (image[lowerX][lowerY][lowerZ] & 0x0000ff);
                lXuYlZ = (int) (image[lowerX][upperY][lowerZ] & 0x0000ff);
                uXlYlZ = (int) (image[upperX][lowerY][lowerZ] & 0x0000ff);
                uXuYlZ = (int) (image[upperX][upperY][lowerZ] & 0x0000ff);
                lXlYuZ = (int) (image[lowerX][lowerY][upperZ] & 0x0000ff);
                lXuYuZ = (int) (image[lowerX][upperY][upperZ] & 0x0000ff);
                uXlYuZ = (int) (image[upperX][lowerY][upperZ] & 0x0000ff);
                uXuYuZ = (int) (image[upperX][upperY][upperZ] & 0x0000ff);
                break;
            default:
                throw new IllegalArgumentException("Some wrong color value was given to method, " + color);
        }

        double lYlZ = lerp(lXlYlZ, uXlYlZ, x);
        double lYuZ = lerp(lXlYuZ, uXlYuZ, x);
        double uYlZ = lerp(lXuYlZ, uXuYlZ, x);
        double uYuZ = lerp(lXuYuZ, uXuYuZ, x);

        double lY = lerp(lYlZ, lYuZ, z);
        double uY = lerp(uYlZ, uYuZ, z);

        double resultValue = lerp(lY, uY, y);

        return (int) resultValue;
    }

    private static int computeTriCubic(int[][][] image, Vector3d coordinates, Colors color) {
        int xSize = image.length;
        int ySize = image[0].length;
        int zSize = image[0][0].length;
        
        double[][][] knownValues = new double[4][4][4];
        
        int lowerX = (int) (coordinates.getX() - (coordinates.getX() % 1));
        int lowerY = (int) (coordinates.getY() - (coordinates.getY() % 1));
        int lowerZ = (int) (coordinates.getZ() - (coordinates.getZ() % 1));
        
        Vector3d lowerBound = new Vector3d(lowerX-1, lowerY-1, lowerZ-1);
        
        for (int x = 0; x < 4; x++) {
            for (int y = 0; y < 4; y++) {
                for (int z = 0; z < 4; z++) {
                    int xc = (lowerBound.getX()+x < 0) ? 0 : (int) lowerBound.getX()+x;
                    xc = (lowerBound.getX()+x > xSize-1) ? xSize-1 : xc;
                    
                    int yc = (lowerBound.getY()+y < 0) ? 0 : (int) lowerBound.getY()+y;
                    yc = (lowerBound.getY()+y > ySize-1) ? ySize-1 : yc;
                    
                    int zc = (lowerBound.getZ()+z < 0) ? 0 : (int)lowerBound.getZ()+z;
                    zc = (lowerBound.getZ()+z > zSize-1) ? zSize-1 : zc;
                    
                    switch(color){
                        case RED:
                            knownValues[x][y][z] = (int) (image[xc][yc][zc] & 0xff0000) >> 16;
                            break;
                        case GREEN:
                            knownValues[x][y][z] = (int) (image[xc][yc][zc] & 0x00ff00) >> 8;
                            break;
                        case BLUE:
                            knownValues[x][y][z] = (int) (image[xc][yc][zc] & 0x0000ff);
                            break;
                    }
                }
            }
        }        
        return (int) getTriCubic(knownValues, coordinates.getX()%1, coordinates.getY()%1, coordinates.getZ()%1);
    }

    private static Vector3d correctCoordinates(Vector3d coordinates, int[][][] image) {
        int xSize = image.length;
        int ySize = image[0].length;
        int zSize = image[0][0].length;

        //if coordinates were rounded away from image, set it to the border pixel
        if (coordinates.getX() < 0) {
            coordinates.setX(0);
        }
        if (coordinates.getX() > xSize - 1) {
            coordinates.setX(xSize - 1);
        }
        if (coordinates.getY() < 0) {
            coordinates.setY(0);
        }
        if (coordinates.getY() > ySize - 1) {
            coordinates.setY(ySize - 1);
        }
        if (coordinates.getZ() < 0) {
            coordinates.setZ(0);
        }
        if (coordinates.getZ() > zSize - 1) {
            coordinates.setZ(zSize - 1);
        }

        return coordinates;
    }

    private static double lerp(double v0, double v1, double t) {
        return v0 + (v1 - v0) * t;
    }

    static double getCubic(double[] p, double x) {
        double result = p[1] + 0.5 * x * (p[2] - p[0] + x * (2.0 * p[0] - 5.0f * p[1] + 4.0 * p[2] - p[3] + x * (3.0 * (p[1] - p[2]) + p[3] - p[0])));     
        return result;
    }

    private static double getBiCubic(double[][] p, double x, double y) {
        double[] arr = new double[4];
        arr[0] = getCubic(p[0], y);
        arr[1] = getCubic(p[1], y);
        arr[2] = getCubic(p[2], y);
        arr[3] = getCubic(p[3], y);
        return getCubic(arr, x);
    }

    private static double getTriCubic(double[][][] p, double x, double y, double z) {
        double[] arr = new double[4];
        arr[0] = getBiCubic(p[0], y, z);
        arr[1] = getBiCubic(p[1], y, z);
        arr[2] = getBiCubic(p[2], y, z);
        arr[3] = getBiCubic(p[3], y, z);
        return getCubic(arr, x);
    }

}
